<!DOCTYPE html>
<html>
<head>
<title>Multi-Node Path Links</title>
<meta name="description" content="A custom Link routing that goes smoothly through a sequence of Nodes." />
<!-- Copyright 1998-2016 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<link href="../assets/css/goSamples.css" rel="stylesheet" type="text/css" />  <!-- you don't need to use this -->
<script src="goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->

<script id="code">
  function init() {
    if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv", // the ID of the DIV HTML element
        {
          initialContentAlignment: go.Spot.Center,
          "Changed": invalidateLinkRoutes,
          "undoManager.isEnabled": true
        });

    myDiagram.nodeTemplate =
      $(go.Node, go.Panel.Auto,
        { locationSpot: go.Spot.Center },
        new go.Binding("location", "loc", go.Point.parse),
        $(go.Shape,
          { figure: "Circle", fill: "white" },
          new go.Binding("fill", "color")),
        $(go.TextBlock,
          { font: "bold 11pt sans-serif" },
          new go.Binding("text"))
      );

    myDiagram.linkTemplate =
      $(MultiNodePathLink,  // subclass of Link, defined below
        go.Link.Bezier,
        { layerName: "Background", toShortLength: 4 },
        $(go.Shape, { strokeWidth: 4 },
          new go.Binding("stroke", "color")),
        $(go.Shape, { toArrow: "Standard", scale: 3, strokeWidth: 0 },
          new go.Binding("fill", "color"))
      );

    function invalidateLinkRoutes(e) {
      // when a Node is moved, invalidate the route for all MultiNodePathLinks that go through it
      if (e.change === go.ChangedEvent.Property && e.propertyName === "location" && e.object instanceof go.Node) {
        var diagram = e.diagram;
        var node = e.object;
        if (node._PathLinks) {
          node._PathLinks.each(function(l) { l.invalidateRoute(); });
        }
      } else if (e.change === go.ChangedEvent.Remove && e.object instanceof go.Layer) {
        // when a Node is deleted that has MultiNodePathLinks going through it, invalidate those link routes
        if (e.oldValue instanceof go.Node) {
          var node = e.oldValue;
          if (node._PathLinks) {
            node._PathLinks.each(function(l) { l.invalidateRoute(); });
          }
        } else if (e.oldValue instanceof MultiNodePathLink) {
          // when deleting a MultiNodePathLink, remove all references to it in Node._PathLinks
          var link = e.oldValue;
          var diagram = e.diagram;
          var midkeys = link.data.path;
          if (Array.isArray(midkeys)) {
            for (var i = 0; i < midkeys.length; i++) {
              var node = diagram.findNodeForKey(midkeys[i]);
              if (node !== null && node._PathLinks) node._PathLinks.remove(link);
            }
          }
        }
      }
    }

    // create a few nodes and links
    myDiagram.model = new go.GraphLinksModel([
        { key: 1, text: "Alpha", color: "lightyellow", loc: "0 0" },
        { key: 2, text: "Beta", color: "brown", loc: "200 0" },
        { key: 3, text: "Gamma", color: "green", loc: "300 100" },
        { key: 4, text: "Delta", color: "slateblue", loc: "100 200" },
        { key: 5, text: "Epsilon", color: "aquamarine", loc: "300 350" },
        { key: 6, text: "Zeta", color: "tomato", loc: "0 100" },
        { key: 7, text: "Eta", color: "goldenrod", loc: "0 300" },
        { key: 8, text: "Theta", color: "orange", loc: "300 200" },
    ], [
        { from: 1, to: 5, path: [2, 3, 4], color: "blue" },
        { from: 6, to: 5, path: [7, 4, 8], color: "red" }
    ]);
  }


  function MultiNodePathLink() {
    go.Link.call(this);
  }
  go.Diagram.inherit(MultiNodePathLink, go.Link);

  // ignores this.routing, this.adjusting, this.corner, this.smoothness, this.curviness
  /** @override */
  MultiNodePathLink.prototype.computePoints = function() {
    // get the list of Nodes that should be along the path
    var nodes = [];
    if (this.fromNode !== null && this.fromNode.location.isReal()) {
      nodes.push(this.fromNode);
    }
    var midkeys = this.data.path;
    if (Array.isArray(midkeys)) {
      var diagram = this.diagram;
      for (var i = 0; i < midkeys.length; i++) {
        var node = diagram.findNodeForKey(midkeys[i]);
        if (node instanceof go.Node && node.location.isReal()) {
          nodes.push(node);
          // Optimization?: remember on each path Node all of
          // the MultiNodePathLinks that go through it;
          // but this optimization requires maintaining this cache
          // in a Diagram Changed event listener.
          var set = node._PathLinks;
          if (!set) set = node._PathLinks = new go.Set(go.Link);
          set.add(this);
        }
      }
    }
    if (this.toNode !== null && this.toNode.location.isReal()) {
      nodes.push(this.toNode);
    }

    // now do the routing
    this.clearPoints();
    var prevloc = null;
    var thisloc = null;
    var nextloc = null;
    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i];
      thisloc = node.location;
      nextloc = (i < nodes.length - 1) ? nodes[i + 1].location : null;

      var prevpt = null;
      var nextpt = null;
      if (this.curve === go.Link.Bezier) {
        if (prevloc !== null && nextloc !== null) {
          var prevang = thisloc.directionPoint(prevloc);
          var nextang = thisloc.directionPoint(nextloc);
          var avg = (prevang + nextang) / 2;
          var clockwise = prevang > nextang;
          if (Math.abs(prevang - nextang) > 180) {
            avg += 180;
            clockwise = !clockwise;
          }
          if (avg >= 360) avg -= 360;
          prevpt = new go.Point(Math.sqrt(thisloc.distanceSquaredPoint(prevloc)) / 4, 0);
          prevpt.rotate(avg + (clockwise ? 90 : -90));
          prevpt.add(thisloc);
          nextpt = new go.Point(Math.sqrt(thisloc.distanceSquaredPoint(nextloc)) / 4, 0);
          nextpt.rotate(avg - (clockwise ? 90 : -90));
          nextpt.add(thisloc);
        } else if (nextloc !== null) {
          prevpt = null;
          nextpt = thisloc;  // fix this point after the loop
        } else if (prevloc !== null) {
          var lastpt = this.getPoint(this.pointsCount - 1);
          prevpt = thisloc;  // fix this point after the loop
          nextpt = null;
        }
      }

      if (prevpt !== null) this.addPoint(prevpt);
      this.addPoint(thisloc);
      if (nextpt !== null) this.addPoint(nextpt);
      prevloc = thisloc;
    }

    // fix up the end points when it's Bezier
    if (this.curve === go.Link.Bezier) {
      // fix up the first point and the first control point
      var start = this.getLinkPointFromPoint(this.fromNode, this.fromPort, this.fromPort.getDocumentPoint(go.Spot.Center), this.getPoint(3), true);
      var ctrl2 = this.getPoint(2);
      this.setPoint(0, start);
      this.setPoint(1, new go.Point((start.x * 3 + ctrl2.x) / 4, (start.y * 3 + ctrl2.y) / 4));
      // fix up the last point and the last control point
      var end = this.getLinkPointFromPoint(this.toNode, this.toPort, this.toPort.getDocumentPoint(go.Spot.Center), this.getPoint(this.pointsCount - 4), false);
      var ctrl1 = this.getPoint(this.pointsCount - 3);
      this.setPoint(this.pointsCount - 2, new go.Point((end.x * 3 + ctrl1.x) / 4, (end.y * 3 + ctrl1.y) / 4));
      this.setPoint(this.pointsCount - 1, end);
    }

    return true;
  };
  // end MultiNodePathLink class
  </script>
</head>
<body onload="init()">
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px blue; width:100%; height:700px; min-width: 200px"></div>
  <p>
  This sample demonstrates customization of the <a>Link</a>'s routing to go through multiple Nodes.
  The nodes are specified by key in the link data's "path" property, which must be an Array of node keys.
  </p>
  <p>
  As the user drags around Nodes on the "path", the routing is automatically recomputed to maintain a smooth curve.
  </p>
</div>
</body>
</html>
